package mylittleinvestigations;

import mylittleinvestigations.CaseInformation.CaseXmlWriter;
import mylittleinvestigations.CaseInformation.Case;
import mylittleinvestigations.CaseInformation.CaseXmlReader;
import mylittleinvestigations.CaseInformation.CaseParseException;
import java.io.IOException;
import mylittleinvestigations.FieldCharacters.*;
import java.util.*;
import mylittleinvestigations.EasingFunctions.EasingFunction;
import mylittleinvestigations.EasingFunctions.LinearEase;
import org.newdawn.slick.*;

public class FieldCutscene
{
    private ArrayList<FieldCutscenePhase> phaseList;
    private HashMap<String, FieldCharacter> idToCharacterDictionary;
    
    private HashMap<FieldCharacter, CharacterDirection> characterToOriginalCharacterDirectionDictionary;
    private HashMap<FieldCharacter, FieldCharacterDirection> characterToOriginalFieldCharacterDirectionDictionary;
    private HashMap<FieldCharacter, Vector2> characterToOriginalPositionDictionary;
    private HashMap<FieldCharacter, FieldCharacterState> characterToOriginalStateDictionary;

    private FieldCutscenePhase currentPhase;
    private int currentPhaseId;
    
    FieldCutsceneConcurrentMovements currentConcurrentMovements;
    
    private Sprite backgroundSprite;
    private double backgroundSpriteOpacity;
    private Sprite replacementBackgroundSprite;
    private double replacementBackgroundSpriteOpacity;
    
    private Vector2 initialCameraPosition;
    
    private boolean preventInitialBgm;
    private String initialBgmReplacement;
    
    private boolean preventInitialAmbiance;
    private String initialAmbianceReplacement;

    public FieldCutscene()
    {
        this.phaseList = new ArrayList<FieldCutscenePhase>();
        this.idToCharacterDictionary = new HashMap<String, FieldCharacter>();
        this.isEnabled = false;
        this.hasCompleted = false;
        this.currentConcurrentMovements = null;
        
        this.backgroundSprite = null;
        this.backgroundSpriteOpacity = 0;
        this.replacementBackgroundSprite = null;
        this.replacementBackgroundSpriteOpacity = 0;
        
        this.preventInitialBgm = false;
        this.initialBgmReplacement = null;
        this.preventInitialAmbiance = false;
        this.initialAmbianceReplacement = null;
    }
    
    private String id;

    public String getId() { return this.id; }
    public void setId(String id) { this.id = id; }
    
    private boolean isEnabled;

    public boolean getIsEnabled() { return this.isEnabled; }
    public void setIsEnabled(boolean isEnabled) { this.isEnabled = isEnabled; }
    
    private boolean isFinished;

    public boolean getIsFinished() { return this.isFinished; }
    public void setIsFinished(boolean isFinished) { this.isFinished = isFinished; }
    
    private boolean hasCompleted;

    public boolean getHasCompleted() { return this.hasCompleted; }
    public void setHasCompleted(boolean hasCompleted) { this.hasCompleted = hasCompleted; }
    
    public void setBackgroundSprite(Sprite backgroundSprite) { this.backgroundSprite = backgroundSprite; }
    public void setBackgroundSpriteOpacity(double backgroundSpriteOpacity) { this.backgroundSpriteOpacity = backgroundSpriteOpacity; }
    public void setReplacementBackgroundSprite(Sprite replacementBackgroundSprite) { this.replacementBackgroundSprite = replacementBackgroundSprite; }
    public void setReplacementBackgroundSpriteOpacity(double replacementBackgroundSpriteOpacity) { this.replacementBackgroundSpriteOpacity = replacementBackgroundSpriteOpacity; }
    
    public void setInitialCameraPosition(Vector2 initialCameraPosition) { this.initialCameraPosition = initialCameraPosition; }
    
    private String partnerCharacterId;
    
    public String getPartnerCharacterId() { return this.partnerCharacterId; }
    public void setPartnerCharacterId(String partnerCharacterId) { this.partnerCharacterId = partnerCharacterId; }

    public void AddCharacter(String characterId, FieldCharacter character)
    {
        this.idToCharacterDictionary.put(characterId, character);
    }
    
    public void SetBackground(String backgroundTextureId, int msFadeDuration)
    {
        this.phaseList.add(new FieldCutsceneSetBackground(backgroundTextureId, msFadeDuration));
    }

    public void AddConversation(Conversation conversation)
    {
        Encounter encounter = new Encounter();
        encounter.setOneShotConversation(conversation);
        this.phaseList.add(new FieldCutsceneConversation(encounter));
    }

    public void AddMovement(String characterId, Vector2 targetPosition, FieldCharacterState movementState)
    {
        this.phaseList.add(new FieldCutsceneMovement(characterId, targetPosition, movementState));
    }

    public void AddOrient(String characterId, CharacterDirection direction, FieldCharacterDirection spriteDirection)
    {
        this.phaseList.add(new FieldCutsceneOrient(characterId, direction, spriteDirection));
    }
    
    public void CenterCamera(String characterId)
    {
        this.phaseList.add(new FieldCutsceneCenterCamera(characterId));
    }
    
    public void CenterCamera(Vector2 cameraCenterPosition)
    {
        this.phaseList.add(new FieldCutsceneCenterCamera(cameraCenterPosition));
    }
    
    public void BeginConcurrentMovements()
    {
        this.currentConcurrentMovements = new FieldCutsceneConcurrentMovements();
    }

    public void ConcurrentMovementsAddMovement(String characterId, Vector2 targetPosition, FieldCharacterState movementState)
    {
        if (this.currentConcurrentMovements != null)
        {
            this.currentConcurrentMovements.AddMovement(characterId, targetPosition, movementState);
        }
    }

    public void ConcurrentMovementsAddOrient(String characterId, CharacterDirection direction, FieldCharacterDirection spriteDirection)
    {
        if (this.currentConcurrentMovements != null)
        {
            this.currentConcurrentMovements.AddOrient(characterId, direction, spriteDirection);
        }
    }
    
    public void ConcurrentMovementsAddCenterCamera(String characterId)
    {
        if (this.currentConcurrentMovements != null)
        {
            this.currentConcurrentMovements.AddCenterCamera(characterId);
        }
    }
    
    public void ConcurrentMovementsAddCenterCamera(Vector2 cameraCenterPosition)
    {
        if (this.currentConcurrentMovements != null)
        {
            this.currentConcurrentMovements.AddCenterCamera(cameraCenterPosition);
        }
    }
    
    public void ConcurrentMovementsAddMillisecondDelayBeforeBegin(int msWait)
    {
        if (this.currentConcurrentMovements != null)
        {
            this.currentConcurrentMovements.setMillisecondDelayBeforeBegin(this.currentConcurrentMovements.getMillisecondDelayBeforeBegin() + msWait);
        }
    }
    
    public void ConcurrentMovementsSetMillisecondDelayBeforeBegin(int msWait)
    {
        if (this.currentConcurrentMovements != null)
        {
            this.currentConcurrentMovements.setMillisecondDelayBeforeBegin(msWait);
        }
    }

    public void FinishConcurrentMovements()
    {
        this.phaseList.add(this.currentConcurrentMovements);
        this.currentConcurrentMovements = null;
    }
    
    public void Pause(int msDuration)
    {
        this.phaseList.add(new FieldCutscenePause(msDuration));
    }

    public void PlayBgm(String bgmId)
    {
        this.phaseList.add(new FieldCutscenePlayBgm(bgmId));
    }

    public void StopBgm(boolean isInstant)
    {
        this.phaseList.add(new FieldCutsceneStopBgm(isInstant));
    }

    public void PreventInitialBgm()
    {
        this.preventInitialBgm = true;
    }

    public void ReplaceInitialBgm(String initialBgmReplacement)
    {
        this.preventInitialBgm = true;
        this.initialBgmReplacement = initialBgmReplacement;
    }

    public void PlayAmbiance(String ambianceSfxId)
    {
        this.phaseList.add(new FieldCutscenePlayAmbiance(ambianceSfxId));
    }

    public void StopAmbiance(boolean isInstant)
    {
        this.phaseList.add(new FieldCutsceneStopAmbiance(isInstant));
    }

    public void PreventInitialAmbiance()
    {
        this.preventInitialAmbiance = true;
    }

    public void ReplaceInitialAmbiance(String initialAmbianceReplacement)
    {
        this.preventInitialAmbiance = true;
        this.initialAmbianceReplacement = initialAmbianceReplacement;
    }

    public void EndCase()
    {
        this.phaseList.add(new FieldCutsceneEndCase(false /* completesCase */));
    }

    public void CompleteCase()
    {
        this.phaseList.add(new FieldCutsceneEndCase(true /* completesCase */));
    }

    public void Begin() throws CaseParseException
    {
        this.setIsFinished(false);
        this.currentPhaseId = 0;
        this.StartNextPhase();

        if (this.idToCharacterDictionary.containsKey("PlayerCharacter"))
        {
            // We store a copy of the player character when compiling the case,
            // so we need to replace it with the actual player character.
            FieldCharacter playerCharacterStored = this.idToCharacterDictionary.get("PlayerCharacter");
            FieldCharacter actualPlayerCharacter = Case.getInstance().getFieldCharacterManager().GetPlayerCharacter();
            
            CharacterDirection originalCharacterDirection = this.characterToOriginalCharacterDirectionDictionary.remove(playerCharacterStored);
            FieldCharacterDirection originalFieldCharacterDirection = this.characterToOriginalFieldCharacterDirectionDictionary.remove(playerCharacterStored);
            Vector2 originalPosition = this.characterToOriginalPositionDictionary.remove(playerCharacterStored);
            FieldCharacterState originalState = this.characterToOriginalStateDictionary.remove(playerCharacterStored);
            
            this.characterToOriginalCharacterDirectionDictionary.put(actualPlayerCharacter, originalCharacterDirection);
            this.characterToOriginalFieldCharacterDirectionDictionary.put(actualPlayerCharacter, originalFieldCharacterDirection);
            this.characterToOriginalPositionDictionary.put(actualPlayerCharacter, originalPosition.Clone());
            this.characterToOriginalStateDictionary.put(actualPlayerCharacter, originalState);

            actualPlayerCharacter.setPosition(playerCharacterStored.getPosition());
            actualPlayerCharacter.setDirection(playerCharacterStored.getDirection());

            this.idToCharacterDictionary.put("PlayerCharacter", actualPlayerCharacter);
        }
        
        FieldCharacter[] valuesArray = this.idToCharacterDictionary.values().toArray(new FieldCharacter[0]);
        
        for (int i = 0; i < valuesArray.length; i++)
        {
            valuesArray[i].Begin();
        }
    }

    public void Update(GameContainer gc, int delta) throws CaseParseException
    {
        if (this.getIsFinished())
        {
            return;
        }

        if (this.currentPhase.getIsFinished())
        {
            this.StartNextPhase();

            if (this.getIsFinished())
            {
                return;
            }
        }

        if (this.currentPhase.getAllowsCharacterUpdates())
        {
            FieldCharacter[] valuesArray = this.idToCharacterDictionary.values().toArray(new FieldCharacter[0]);

            for (int i = 0; i < valuesArray.length; i++)
            {
                valuesArray[i].Update(gc, delta);
            }
        }

        this.currentPhase.Update(gc, delta);
    }

    public void Draw(GameContainer gc, Graphics g, Vector2 offsetVector) throws SlickException, CaseParseException
    {
        if (this.getIsFinished())
        {
            return;
        }
        
        if (this.backgroundSprite == null || this.backgroundSpriteOpacity < 1)
        {
            ArrayList<FieldCharacter> charactersInZOrder = new ArrayList<FieldCharacter>(this.idToCharacterDictionary.values());
            Collections.sort(charactersInZOrder, new FieldCharacterComparator());

            for (int i = 0; i < charactersInZOrder.size(); i++)
            {
                charactersInZOrder.get(i).Draw(gc, g, offsetVector);
            }
        }
        
        if (this.backgroundSprite != null)
        {
            this.backgroundSprite.Draw(gc, g, new Vector2(0, 0), new Color(1.0f, 1.0f, 1.0f, (float)this.backgroundSpriteOpacity));
            
            if (this.replacementBackgroundSprite != null)
            {
                this.replacementBackgroundSprite.Draw(gc, g, new Vector2(0, 0), new Color(1.0f, 1.0f, 1.0f, (float)this.replacementBackgroundSpriteOpacity));
            }
        }

        this.currentPhase.Draw(gc, g);
    }
    
    public void Reset()
    {
        this.backgroundSprite = null;
        this.replacementBackgroundSprite = null;
        
        for (FieldCharacter character : this.idToCharacterDictionary.values())
        {
            character.Reset(
                this.characterToOriginalCharacterDirectionDictionary.get(character),
                this.characterToOriginalFieldCharacterDirectionDictionary.get(character),
                this.characterToOriginalPositionDictionary.get(character).Clone(),
                this.characterToOriginalStateDictionary.get(character));
        }
    }

    private void StartNextPhase() throws CaseParseException
    {
        if (this.currentPhaseId < this.phaseList.size())
        {
            this.currentPhase = this.phaseList.get(this.currentPhaseId);
            this.currentPhase.Begin(this);
            this.currentPhase.Update(null, 0);
            this.currentPhaseId++;
        }
        else
        {
            this.setIsFinished(true);
            this.setHasCompleted(true);
        }
    }

    public void SaveToXml(CaseXmlWriter writer) throws IOException
    {
        writer.StartElement("FieldCutscene");
        
        Case.getInstance().getSpriteManager().StartGroup("FieldCutscene" + this.id);
        
        writer.WriteTextElement("Id", this.id);
        
        writer.StartElement("PhaseList");
        
        for (int i = 0; i < this.phaseList.size(); i++)
        {
            writer.StartElement("Entry");
            this.phaseList.get(i).SaveToXml(writer);
            writer.EndElement();
        }
        
        writer.EndElement();
        
        writer.StartElement("IdToCharacterDictionary");
        String[] ids = this.idToCharacterDictionary.keySet().toArray(new String[0]);
        
        for (int i = 0; i < ids.length; i++)
        {
            writer.StartElement("Entry");
            String id = ids[i];
            writer.WriteTextElement("Id", id);
            this.idToCharacterDictionary.get(id).SaveToXml(writer);
            writer.EndElement();
        }
        
        writer.EndElement();
        
        writer.WriteBooleanElement("IsEnabled", this.isEnabled);
        writer.WriteBooleanElement("PreventInitialBgm", this.preventInitialBgm);
        
        if (this.initialBgmReplacement != null)
        {
            writer.WriteTextElement("InitialBgmReplacement", this.initialBgmReplacement);
        }
        
        writer.WriteBooleanElement("PreventInitialAmbiance", this.preventInitialAmbiance);
        
        if (this.initialAmbianceReplacement != null)
        {
            writer.WriteTextElement("InitialAmbianceReplacement", this.initialAmbianceReplacement);
        }
        
        writer.StartElement("InitialCameraPosition");
        this.initialCameraPosition.SaveToXml(writer);
        writer.EndElement();
        
        Case.getInstance().getSpriteManager().EndGroup();

        writer.EndElement();
    }

    public FieldCutscene(CaseXmlReader reader)
    {
        this.phaseList = new ArrayList<FieldCutscenePhase>();
        this.idToCharacterDictionary = new HashMap<String, FieldCharacter>();
        this.characterToOriginalCharacterDirectionDictionary = new HashMap<FieldCharacter, CharacterDirection>();
        this.characterToOriginalFieldCharacterDirectionDictionary = new HashMap<FieldCharacter, FieldCharacterDirection>();
        this.characterToOriginalPositionDictionary = new HashMap<FieldCharacter, Vector2>();
        this.characterToOriginalStateDictionary = new HashMap<FieldCharacter, FieldCharacterState>();
        this.isEnabled = false;
        this.hasCompleted = false;
        this.currentConcurrentMovements = null;
        
        reader.StartElement("FieldCutscene");
        
        reader.StartElement("PhaseList");
        reader.StartList("Entry");
        
        while (reader.MoveToNextListItem())
        {
            this.phaseList.add(this.GetPhaseForNextElement(reader));
        }
        
        reader.EndElement();
        
        reader.StartElement("IdToCharacterDictionary");
        reader.StartList("Entry");
        
        while (reader.MoveToNextListItem())
        {
            String id = reader.ReadTextElement("Id");
            FieldCharacter character = new FieldCharacter(reader);
            this.idToCharacterDictionary.put(id, character);
            
            this.characterToOriginalCharacterDirectionDictionary.put(character, character.getDirection());
            this.characterToOriginalFieldCharacterDirectionDictionary.put(character, character.getSpriteDirection());
            this.characterToOriginalPositionDictionary.put(character, character.getPosition().Clone());
            this.characterToOriginalStateDictionary.put(character, character.getState());
        }
        
        reader.EndElement();
        
        this.isEnabled = reader.ReadBooleanElement("IsEnabled");
        this.preventInitialBgm = reader.ReadBooleanElement("PreventInitialBgm");
        
        if (reader.ElementExists("InitialBgmReplacement"))
        {
            this.initialBgmReplacement = reader.ReadTextElement("InitialBgmReplacement");
        }
        
        this.preventInitialAmbiance = reader.ReadBooleanElement("PreventInitialAmbiance");
        
        if (reader.ElementExists("InitialAmbianceReplacement"))
        {
            this.initialAmbianceReplacement = reader.ReadTextElement("InitialAmbianceReplacement");
        }
        
        reader.StartElement("InitialCameraPosition");
        this.initialCameraPosition = new Vector2(reader);
        reader.EndElement();
        
        reader.EndElement();
    }
    
    private FieldCutscenePhase GetPhaseForNextElement(CaseXmlReader reader)
    {
        if (reader.ElementExists("FieldCutsceneConversation"))
        {
            return new FieldCutsceneConversation(reader);
        }
        else if (reader.ElementExists("FieldCutsceneConcurrentMovements"))
        {
            return new FieldCutsceneConcurrentMovements(reader);
        }
        else if (reader.ElementExists("FieldCutsceneMovement"))
        {
            return new FieldCutsceneMovement(reader);
        }
        else if (reader.ElementExists("FieldCutsceneOrient"))
        {
            return new FieldCutsceneOrient(reader);
        }
        else if (reader.ElementExists("FieldCutsceneSetBackground"))
        {
            return new FieldCutsceneSetBackground(reader);
        }
        else if (reader.ElementExists("FieldCutsceneCenterCamera"))
        {
            return new FieldCutsceneCenterCamera(reader);
        }
        else
        {
            throw new IllegalArgumentException("Unknown phase type.");
        }
    }
    
    private class StateDirectionPair
    {
        private FieldCharacterState state;
        private FieldCharacterDirection direction;
        
        public StateDirectionPair(FieldCharacterState state, FieldCharacterDirection direction)
        {
            this.state = state;
            this.direction = direction;
        }
        
        public FieldCharacterState getState() { return this.state; }
        public FieldCharacterDirection getDirection() { return this.direction; }
        
        @Override
        public boolean equals(Object obj)
        {
            if (!(obj instanceof StateDirectionPair))
            {
                return false;
            }
            
            StateDirectionPair otherPair = (StateDirectionPair)obj;
            return this.state == otherPair.state && this.direction == otherPair.direction;
        }
    }

    public void markChildSprites() throws CaseParseException
    {
        HashMap<String, Vector2> idToCharacterPositionMap = new HashMap<String, Vector2>();
        HashMap<String, ArrayList<StateDirectionPair> > characterIdToStateDirectionPairMap = new HashMap<String, ArrayList<StateDirectionPair> >();
                
        for (FieldCharacter fc : idToCharacterDictionary.values())
        {
            idToCharacterPositionMap.put(fc.getId(), fc.getPosition());
            this.AddPositionInformation(characterIdToStateDirectionPairMap, new PositionUpdateInformation(fc.getId(), fc.getSpriteDirection(), FieldCharacterState.Standing));
        }
        
        for (FieldCutscenePhase fcp : phaseList)
        {
            PositionUpdateInformation[] newPositionInformationList = fcp.getUpdatedPositions(idToCharacterPositionMap, this.partnerCharacterId);
            
            if (newPositionInformationList != null)
            {
                for (PositionUpdateInformation newPositionInformation : newPositionInformationList)
                {
                    this.AddPositionInformation(characterIdToStateDirectionPairMap, newPositionInformation);
                }
            }
        }
        
        for (FieldCharacter fc : idToCharacterDictionary.values())
        {
            if (characterIdToStateDirectionPairMap.containsKey(fc.getId()))
            {
                ArrayList<StateDirectionPair> stateDirectionPairList = characterIdToStateDirectionPairMap.get(fc.getId());
                
                for (StateDirectionPair stateDirectionPair : stateDirectionPairList)
                {
                    fc.markChildSprites(this.getId(), stateDirectionPair.getState(), stateDirectionPair.getDirection());
                }
            }
        }
        
        for (FieldCutscenePhase fcp : phaseList)
        {
            fcp.markChildSprites(this.getId());
        }
    }
    
    private void AddPositionInformation(HashMap<String, ArrayList<StateDirectionPair> > characterIdToStateDirectionPairMap, PositionUpdateInformation positionUpdateInformation)
    {
        ArrayList<StateDirectionPair> stateDirectionPairList = null;

        if (characterIdToStateDirectionPairMap.containsKey(positionUpdateInformation.getCharacterId()))
        {
            stateDirectionPairList = characterIdToStateDirectionPairMap.get(positionUpdateInformation.getCharacterId());
        }
        else
        {
            stateDirectionPairList = new ArrayList<StateDirectionPair>();
            characterIdToStateDirectionPairMap.put(positionUpdateInformation.getCharacterId(), stateDirectionPairList);
        }

        StateDirectionPair pair = new StateDirectionPair(positionUpdateInformation.getTransitionState(), positionUpdateInformation.getNewDirection());

        if (!stateDirectionPairList.contains(pair))
        {
            stateDirectionPairList.add(pair);
        }

        if (positionUpdateInformation.getTransitionState() != FieldCharacterState.Standing)
        {
            pair = new StateDirectionPair(FieldCharacterState.Standing, positionUpdateInformation.getNewDirection());

            if (!stateDirectionPairList.contains(pair))
            {
                stateDirectionPairList.add(pair);
            }
        }
    }
    
    private class FieldCharacterComparator implements Comparator<FieldCharacter>
    {
        public int compare(FieldCharacter fieldCharacter1, FieldCharacter fieldCharacter2)
        {
            return (int)((fieldCharacter1.getPosition().getY() + fieldCharacter1.getAnchorPosition()) - (fieldCharacter2.getPosition().getY() + fieldCharacter2.getAnchorPosition()));
        }
    }
    
    private class PositionUpdateInformation
    {
        private String characterId;
        private FieldCharacterDirection newDirection;
        private FieldCharacterState transitionState;

        public PositionUpdateInformation(String characterId, FieldCharacterDirection newDirection, FieldCharacterState transitionState)
        {
            this.characterId = characterId;
            this.newDirection = newDirection;
            this.transitionState = transitionState;
        }

        public String getCharacterId() { return this.characterId; }
        public FieldCharacterDirection getNewDirection() { return this.newDirection; }
        public FieldCharacterState getTransitionState() { return this.transitionState; }
    }
    
    private abstract class FieldCutscenePhase
    {
        
        protected FieldCutscene parentCutscene;

        public FieldCutscenePhase()
        {
        }
        
        private boolean isFinished;
        
        public boolean getIsFinished() { return this.isFinished; }
        protected void setIsFinished(boolean isFinished) { this.isFinished = isFinished; }

        public boolean getAllowsCharacterUpdates()
        {
            return true;
        }
        
        public PositionUpdateInformation[] getUpdatedPositions(HashMap<String, Vector2> idToCharacterPositionMap, String partnerCharacterId)
        {
            return null;
        }
        
        protected int millisecondDelayBeforeBegin;
        
        public int getMillisecondDelayBeforeBegin() { return this.millisecondDelayBeforeBegin; }
        protected void setMillisecondDelayBeforeBegin(int millisecondDelayBeforeBegin) { this.millisecondDelayBeforeBegin = millisecondDelayBeforeBegin; }

        public void Begin(FieldCutscene parentCutscene)
        {
            this.setIsFinished(false);
            this.parentCutscene = parentCutscene;
        }

        public abstract void Update(GameContainer gc, int delta) throws CaseParseException;
        public abstract void Draw(GameContainer gc, Graphics g) throws SlickException, CaseParseException;
        public abstract void SaveToXml(CaseXmlWriter writer) throws IOException;
        
        public void markChildSprites(String parentMark) throws CaseParseException
        {
        }
    }

    private class FieldCutsceneConversation extends FieldCutscenePhase
    {
        private Encounter encounter;

        public FieldCutsceneConversation(Encounter encounter)
        {
            this.encounter = encounter;
        }

        @Override
        public boolean getAllowsCharacterUpdates()
        {
            return false;
        }

        @Override
        public void Begin(FieldCutscene parentCutscene)
        {
            super.Begin(parentCutscene);
            this.encounter.Begin();
        }

        @Override
        public void Update(GameContainer gc, int delta) throws CaseParseException
        {
            this.encounter.Update(gc, delta);

            if (this.encounter.getIsFinished())
            {
                this.setIsFinished(true);
            }
        }

        @Override
        public void Draw(GameContainer gc, Graphics g) throws SlickException
        {
            this.encounter.Draw(gc, g);
        }
        
        @Override
        public void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("FieldCutsceneConversation");
            writer.WriteIntElement("MillisecondDelayBeforeBegin", this.millisecondDelayBeforeBegin);
            this.encounter.SaveToXml(writer);
            writer.EndElement();
        }

        private FieldCutsceneConversation(CaseXmlReader reader)
        {
            reader.StartElement("FieldCutsceneConversation");
            this.millisecondDelayBeforeBegin = reader.ReadIntElement("MillisecondDelayBeforeBegin");
            this.encounter = new Encounter(reader);
            reader.EndElement();
        }
        
        @Override
        public void markChildSprites(String parentMark) throws CaseParseException
        {
            this.encounter.markChildSprites(parentMark);
        }
    }

    private class FieldCutsceneConcurrentMovements extends FieldCutscenePhase
    {
        private ArrayList<FieldCutscenePhase> movementList;
        private double millisecondsElapsed;

        public FieldCutsceneConcurrentMovements()
        {
            this.movementList = new ArrayList<FieldCutscenePhase>();
        }

        public void AddMovement(String characterId, Vector2 targetPosition, FieldCharacterState movementState)
        {
            FieldCutsceneMovement movement = new FieldCutsceneMovement(characterId, targetPosition, movementState);
            movement.setMillisecondDelayBeforeBegin(this.getMillisecondDelayBeforeBegin());

            this.movementList.add(movement);
        }

        public void AddOrient(String characterId, CharacterDirection direction, FieldCharacterDirection spriteDirection)
        {
            FieldCutsceneOrient orient = new FieldCutsceneOrient(characterId, direction, spriteDirection);
            orient.setMillisecondDelayBeforeBegin(this.getMillisecondDelayBeforeBegin());

            this.movementList.add(orient);
        }

        private void AddCenterCamera(String characterId)
        {
            FieldCutsceneCenterCamera centerCamera = new FieldCutsceneCenterCamera(characterId);
            centerCamera.setMillisecondDelayBeforeBegin(this.getMillisecondDelayBeforeBegin());

            this.movementList.add(centerCamera);
        }

        private void AddCenterCamera(Vector2 cameraCenterPosition)
        {
            FieldCutsceneCenterCamera centerCamera = new FieldCutsceneCenterCamera(cameraCenterPosition);
            centerCamera.setMillisecondDelayBeforeBegin(this.getMillisecondDelayBeforeBegin());

            this.movementList.add(centerCamera);
        }
        
        @Override
        public PositionUpdateInformation[] getUpdatedPositions(HashMap<String, Vector2> idToCharacterPositionMap, String partnerCharacterId)
        {
            ArrayList<PositionUpdateInformation> updateInformationList = new ArrayList<PositionUpdateInformation>();
            
            for (FieldCutscenePhase phase : this.movementList)
            {
                PositionUpdateInformation[] phaseUpdateInformationList = phase.getUpdatedPositions(idToCharacterPositionMap, partnerCharacterId);
                
                if (phaseUpdateInformationList != null)
                {
                    updateInformationList.addAll(Arrays.asList(phaseUpdateInformationList));
                }
            }
            
            return updateInformationList.toArray(new PositionUpdateInformation[0]);
        }

        @Override
        public void Begin(FieldCutscene parentCutscene)
        {
            super.Begin(parentCutscene);

            for (int i = 0; i < this.movementList.size(); i++)
            {
                this.movementList.get(i).Begin(parentCutscene);
            }

            this.millisecondsElapsed = 0;
        }

        @Override
        public void Update(GameContainer gc, int delta) throws CaseParseException
        {
            boolean allMovementsComplete = true;
            millisecondsElapsed += delta;

            for (int i = 0; i < this.movementList.size(); i++)
            {
                if (millisecondsElapsed >= this.movementList.get(i).getMillisecondDelayBeforeBegin() && !this.movementList.get(i).getIsFinished())
                {
                    this.movementList.get(i).Update(gc, delta);
                }

                if (!this.movementList.get(i).getIsFinished())
                {
                    allMovementsComplete = false;
                }
            }

            if (allMovementsComplete)
            {
                this.setIsFinished(true);
            }
        }

        @Override
        public void Draw(GameContainer gc, Graphics g)
        {
            // Characters are drawn by the cutscene itself, so nothing to draw here.
        }
        
        @Override
        public void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("FieldCutsceneConcurrentMovements");
            writer.WriteIntElement("MillisecondDelayBeforeBegin", this.millisecondDelayBeforeBegin);
            
            writer.StartElement("MovementList");
            
            for (int i = 0; i < this.movementList.size(); i++)
            {
                writer.StartElement("Entry");
                this.movementList.get(i).SaveToXml(writer);
                writer.EndElement();
            }
            
            writer.EndElement();
            
            writer.EndElement();
        }

        private FieldCutsceneConcurrentMovements(CaseXmlReader reader)
        {
            this.movementList = new ArrayList<FieldCutscenePhase>();
            
            reader.StartElement("FieldCutsceneConcurrentMovements");
            this.millisecondDelayBeforeBegin = reader.ReadIntElement("MillisecondDelayBeforeBegin");
            
            reader.StartElement("MovementList");
            reader.StartList("Entry");
            
            while (reader.MoveToNextListItem())
            {
                this.movementList.add(this.GetPhaseForNextElement(reader));
            }
            
            reader.EndElement();
            
            reader.EndElement();
        }
    
        private FieldCutscenePhase GetPhaseForNextElement(CaseXmlReader reader)
        {
            if (reader.ElementExists("FieldCutsceneConversation"))
            {
                return new FieldCutsceneConversation(reader);
            }
            else if (reader.ElementExists("FieldCutsceneConcurrentMovements"))
            {
                return new FieldCutsceneConcurrentMovements(reader);
            }
            else if (reader.ElementExists("FieldCutsceneMovement"))
            {
                return new FieldCutsceneMovement(reader);
            }
            else if (reader.ElementExists("FieldCutsceneOrient"))
            {
                return new FieldCutsceneOrient(reader);
            }
            else
            {
                throw new IllegalArgumentException("Unknown phase type.");
            }
        }
    }

    public class FieldCutsceneMovement extends FieldCutscenePhase
    {
        private final int MovementSpeed = 250;

        private String characterId;
        private Vector2 targetPosition;
        private FieldCharacterState movementState;

        private FieldCharacter movingCharacter;
        private Vector2 normalizedMovementVector;
        private Vector2 lastPosition;

        public FieldCutsceneMovement(String characterId, Vector2 targetPosition, FieldCharacterState movementState)
        {
            this.characterId = characterId;
            this.targetPosition = targetPosition;
            this.movementState = movementState;
        }
        
        @Override
        public PositionUpdateInformation[] getUpdatedPositions(HashMap<String, Vector2> idToCharacterPositionMap, String partnerCharacterId)
        {
            String characterIdToUse = this.characterId;
            
            if (this.characterId.equals("PlayerCharacter"))
            {
                characterIdToUse = Case.getInstance().getFieldCharacterManager().GetPlayerCharacter().getId();
            }
            else if (this.characterId.equals("PartnerCharacter"))
            {
                characterIdToUse = partnerCharacterId;
            }
            
            Vector2 currentPosition = idToCharacterPositionMap.get(characterIdToUse);
            Vector2 directionVector = this.targetPosition.subtract(currentPosition).normalize();
            
            FieldCharacterDirection newSpriteDirection = FieldCharacterDirection.Side;
            CharacterDirection newDirection = CharacterDirection.Left;
            
            // We'll snap the player's direction vector according to
            // the nearest direction for which we have an animation.
            double angleToHorizontal = Math.acos(directionVector.getX());

            // acos() only returns values from 0 to pi,
            // so to get the full circle we need to check
            // whether we're in the bottom two quandrants,
            // and change the angle to account for this if so.
            if (directionVector.getY() > 0)
            {
                angleToHorizontal = 2 * Math.PI - angleToHorizontal;
            }

            if (angleToHorizontal <= Math.PI / 8 || angleToHorizontal > Math.PI * 15 / 8)
            {
                newSpriteDirection = FieldCharacterDirection.Side;
                newDirection = CharacterDirection.Right;
            }
            else if (angleToHorizontal > Math.PI / 8 && angleToHorizontal <= Math.PI * 3 / 8)
            {
                newSpriteDirection = FieldCharacterDirection.DiagonalUp;
                newDirection = CharacterDirection.Right;
            }
            else if (angleToHorizontal > 3 * Math.PI / 8 && angleToHorizontal <= Math.PI * 5 / 8)
            {
                newSpriteDirection = FieldCharacterDirection.Up;
            }
            else if (angleToHorizontal > 5 * Math.PI / 8 && angleToHorizontal <= Math.PI * 7 / 8)
            {
                newSpriteDirection = FieldCharacterDirection.DiagonalUp;
                newDirection = CharacterDirection.Left;
            }
            else if (angleToHorizontal > 7 * Math.PI / 8 && angleToHorizontal <= Math.PI * 9 / 8)
            {
                newSpriteDirection = FieldCharacterDirection.Side;
                newDirection = CharacterDirection.Left;
            }
            else if (angleToHorizontal > 9 * Math.PI / 8 && angleToHorizontal <= Math.PI * 11 / 8)
            {
                newSpriteDirection = FieldCharacterDirection.DiagonalDown;
                newDirection = CharacterDirection.Left;
            }
            else if (angleToHorizontal > 11 * Math.PI / 8 && angleToHorizontal <= Math.PI * 13 / 8)
            {
                newSpriteDirection = FieldCharacterDirection.Down;
            }
            else if (angleToHorizontal > 13 * Math.PI / 8 && angleToHorizontal <= Math.PI * 15 / 8)
            {
                newSpriteDirection = FieldCharacterDirection.DiagonalDown;
                newDirection = CharacterDirection.Right;
            }
            
            PositionUpdateInformation[] positionUpdateInformationList = new PositionUpdateInformation[1];
            positionUpdateInformationList[0] = new PositionUpdateInformation(characterIdToUse, newSpriteDirection, this.movementState);
            
            idToCharacterPositionMap.put(characterIdToUse, this.targetPosition);
            
            return positionUpdateInformationList;
        }

        @Override
        public void Begin(FieldCutscene parentCutscene)
        {
            super.Begin(parentCutscene);
            this.movingCharacter = parentCutscene.idToCharacterDictionary.get(this.characterId);
            this.movingCharacter.setState(FieldCharacterState.Walking);
            this.normalizedMovementVector = targetPosition.subtract(this.movingCharacter.getPosition()).normalize();
            this.lastPosition = this.movingCharacter.getPosition();
        }

        @Override
        public void Update(GameContainer gc, int delta)
        {
            Vector2 newPosition = this.lastPosition.add(this.normalizedMovementVector.scalarMultiply(MovementSpeed * delta / 1000.0f));
            this.movingCharacter.setPosition(new Vector2((int)newPosition.getX(), (int)newPosition.getY()));
            this.lastPosition = newPosition;

            double angleBetween = Math.acos(this.targetPosition.subtract(newPosition).normalize().dot(this.normalizedMovementVector));

            // If the angle between the two normal vectors is greater than pi / 2,
            // then they're not pointing in the same direction, and we've gone past the target position.
            // We'll back up to the target position and flag that we're done.
            if (angleBetween > Math.PI / 2)
            {
                this.movingCharacter.setPosition(this.targetPosition);
                this.movingCharacter.setState(FieldCharacterState.Standing);
                this.setIsFinished(true);
            }
        }

        @Override
        public void Draw(GameContainer gc, Graphics g)
        {
            // Characters are drawn by the cutscene itself, so nothing to draw here.
        }
        
        @Override
        public void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("FieldCutsceneMovement");
            writer.WriteIntElement("MillisecondDelayBeforeBegin", this.millisecondDelayBeforeBegin);
            writer.WriteTextElement("CharacterId", this.characterId);
            
            writer.StartElement("TargetPosition");
            this.targetPosition.SaveToXml(writer);
            writer.EndElement();
            
            writer.WriteTextElement("MovementState", this.movementState.toString());
            
            writer.EndElement();
        }

        private FieldCutsceneMovement(CaseXmlReader reader)
        {
            reader.StartElement("FieldCutsceneMovement");
            this.millisecondDelayBeforeBegin = reader.ReadIntElement("MillisecondDelayBeforeBegin");
            this.characterId = reader.ReadTextElement("CharacterId");
            
            reader.StartElement("TargetPosition");
            this.targetPosition = new Vector2(reader);
            reader.EndElement();
            
            reader.EndElement();
        }
    }

    public class FieldCutsceneOrient extends FieldCutscenePhase
    {
        private String characterId;
        private FieldCharacter character;
        private CharacterDirection direction;
        private FieldCharacterDirection spriteDirection;

        public FieldCutsceneOrient(String characterId, CharacterDirection direction, FieldCharacterDirection spriteDirection)
        {
            this.characterId = characterId;
            this.direction = direction;
            this.spriteDirection = spriteDirection;
        }
        
        @Override
        public PositionUpdateInformation[] getUpdatedPositions(HashMap<String, Vector2> idToCharacterPositionMap, String partnerCharacterId)
        {
            PositionUpdateInformation[] positionUpdateInformationList = new PositionUpdateInformation[1];
            positionUpdateInformationList[0] = new PositionUpdateInformation(this.characterId, this.spriteDirection, FieldCharacterState.Standing);
            
            return positionUpdateInformationList;
        }

        @Override
        public void Begin(FieldCutscene parentCutscene)
        {
            super.Begin(parentCutscene);
            this.character = parentCutscene.idToCharacterDictionary.get(this.characterId);
        }

        @Override
        public void Update(GameContainer gc, int delta)
        {
            if (!this.getIsFinished())
            {
                this.character.setDirection(this.direction);
                this.character.setSpriteDirection(this.spriteDirection);
                this.setIsFinished(true);
            }
        }

        @Override
        public void Draw(GameContainer gc, Graphics g)
        {
            // Characters are drawn by the cutscene itself, so nothing to draw here.
        }
        
        @Override
        public void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("FieldCutsceneOrient");
            writer.WriteIntElement("MillisecondDelayBeforeBegin", this.millisecondDelayBeforeBegin);
            writer.WriteTextElement("CharacterId", this.characterId);
            writer.WriteTextElement("Direction", this.direction.toString());
            writer.WriteTextElement("SpriteDirection", this.spriteDirection.toString());
            writer.EndElement();
        }

        private FieldCutsceneOrient(CaseXmlReader reader)
        {
            reader.StartElement("FieldCutsceneOrient");
            this.millisecondDelayBeforeBegin = reader.ReadIntElement("MillisecondDelayBeforeBegin");
            this.characterId = reader.ReadTextElement("CharacterId");
            this.direction = CharacterDirection.valueOf(reader.ReadTextElement("Direction"));
            this.spriteDirection = FieldCharacterDirection.valueOf(reader.ReadTextElement("SpriteDirection"));
            reader.EndElement();
        }
    }

    public class FieldCutsceneSetBackground extends FieldCutscenePhase
    {
        private String backgroundTextureId;
        private int msFadeDuration;
        
        private Sprite backgroundSprite;
        private EasingFunction backgroundSpriteOpacityEase;

        public FieldCutsceneSetBackground(String backgroundTextureId, int msFadeDuration)
        {
            this.backgroundTextureId = backgroundTextureId;
            this.msFadeDuration = msFadeDuration;
        }

        @Override
        public void Begin(FieldCutscene parentCutscene)
        {
            super.Begin(parentCutscene);
            
            if (this.backgroundTextureId != null)
            {
                this.backgroundSprite = Case.getInstance().getSpriteManager().GetSpriteFromId(this.backgroundTextureId);
                this.parentCutscene.setReplacementBackgroundSprite(this.backgroundSprite);
                this.parentCutscene.setReplacementBackgroundSpriteOpacity(0);
            }
            
            if (this.msFadeDuration > 0)
            {
                this.backgroundSpriteOpacityEase =
                    new LinearEase(
                        this.backgroundTextureId != null ? 0 : 1,
                        this.backgroundTextureId != null ? 1 : 0,
                        this.msFadeDuration);
                this.backgroundSpriteOpacityEase.Begin();
            }
        }

        @Override
        public void Update(GameContainer gc, int delta)
        {
            if (this.backgroundSpriteOpacityEase != null && !this.backgroundSpriteOpacityEase.getIsFinished())
            {
                this.backgroundSpriteOpacityEase.Update(gc, delta);
                
                if (this.backgroundSpriteOpacityEase.getIsFinished())
                {
                    if (this.backgroundTextureId != null)
                    {
                        this.parentCutscene.setBackgroundSprite(this.backgroundSprite);
                        this.parentCutscene.setBackgroundSpriteOpacity(1);
                        this.parentCutscene.setReplacementBackgroundSprite(null);
                        this.parentCutscene.setReplacementBackgroundSpriteOpacity(0);
                    }
                    else
                    {
                        this.parentCutscene.setBackgroundSprite(null);
                        this.parentCutscene.setBackgroundSpriteOpacity(0);
                        this.parentCutscene.setReplacementBackgroundSprite(null);
                        this.parentCutscene.setReplacementBackgroundSpriteOpacity(0);
                    }
                    
                    this.setIsFinished(true);
                }
                else
                {
                    if (this.backgroundTextureId != null)
                    {
                        this.parentCutscene.setReplacementBackgroundSpriteOpacity(this.backgroundSpriteOpacityEase.GetCurrentValue());
                    }
                    else
                    {
                        this.parentCutscene.setBackgroundSpriteOpacity(this.backgroundSpriteOpacityEase.GetCurrentValue());
                    }
                }
            }
            else
            {
                if (this.backgroundTextureId != null)
                {
                    this.parentCutscene.setBackgroundSprite(this.backgroundSprite);
                    this.parentCutscene.setBackgroundSpriteOpacity(1);
                    this.parentCutscene.setReplacementBackgroundSprite(null);
                    this.parentCutscene.setReplacementBackgroundSpriteOpacity(0);
                }
                else
                {
                    this.parentCutscene.setBackgroundSprite(null);
                    this.parentCutscene.setBackgroundSpriteOpacity(0);
                    this.parentCutscene.setReplacementBackgroundSprite(null);
                    this.parentCutscene.setReplacementBackgroundSpriteOpacity(0);
                }
                    
                this.setIsFinished(true);
            }
        }

        @Override
        public void Draw(GameContainer gc, Graphics g)
        {
            // Nothing to draw - the scene itself is drawn by the cutscene.
        }
        
        @Override
        public void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("FieldCutsceneSetBackground");
            
            if (this.backgroundTextureId != null)
            {
                writer.WriteSpriteId("BackgroundSpriteId", this.backgroundTextureId);
            }
            
            writer.WriteIntElement("MsFadeDuration", this.msFadeDuration);
            writer.EndElement();
        }

        private FieldCutsceneSetBackground(CaseXmlReader reader)
        {
            reader.StartElement("FieldCutsceneSetBackground");
            
            if (reader.ElementExists("BackgroundSpriteId"))
            {
                this.backgroundTextureId = reader.ReadTextElement("BackgroundSpriteId");
            }
            
            this.msFadeDuration = reader.ReadIntElement("MsFadeDuration");
            reader.EndElement();
        }
        
        @Override
        public void markChildSprites(String parentMark)
        {
            if (this.backgroundTextureId != null)
            {
                Case.getInstance().getSpriteManager().GetSpriteFromId(this.backgroundTextureId).markChildSprites(parentMark);
            }
        }
    }

    public class FieldCutsceneCenterCamera extends FieldCutscenePhase
    {
        private String characterId;
        private Vector2 cameraCenterPosition;
        private EasingFunction cameraMovementXEase;
        private EasingFunction cameraMovementYEase;

        public FieldCutsceneCenterCamera(String characterId)
        {
            this.characterId = characterId;
        }

        public FieldCutsceneCenterCamera(Vector2 cameraCenterPosition)
        {
            this.cameraCenterPosition = cameraCenterPosition;
        }

        @Override
        public void Begin(FieldCutscene parentCutscene)
        {
            super.Begin(parentCutscene);
            
            if (this.characterId != null)
            {
                this.cameraCenterPosition = this.parentCutscene.idToCharacterDictionary.get(this.characterId).GetMidPoint();
            }
            
            //this.cameraMovementXEase = new LinearEase()
        }

        @Override
        public void Update(GameContainer gc, int delta)
        {
        }

        @Override
        public void Draw(GameContainer gc, Graphics g)
        {
            // Nothing to draw - the scene itself is drawn by the cutscene.
        }
        
        @Override
        public void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("FieldCutsceneCenterCamera");
            
            writer.WriteIntElement("MillisecondDelayBeforeBegin", this.millisecondDelayBeforeBegin);
            
            if (this.characterId != null)
            {
                writer.WriteTextElement("CharacterId", this.characterId);
            }
            else
            {
                writer.StartElement("CameraCenterPosition");
                this.cameraCenterPosition.SaveToXml(writer);
                writer.EndElement();
            }
            
            writer.EndElement();
        }

        private FieldCutsceneCenterCamera(CaseXmlReader reader)
        {
            reader.StartElement("FieldCutsceneCenterCamera");
            
            this.millisecondDelayBeforeBegin = reader.ReadIntElement("MillisecondDelayBeforeBegin");
            
            if (reader.ElementExists("CharacterId"))
            {
                this.characterId = reader.ReadTextElement("CharacterId");
            }
            else
            {
                reader.StartElement("CameraCenterPosition");
                this.cameraCenterPosition = new Vector2(reader);
                reader.EndElement();
            }
            
            reader.EndElement();
        }
    }

    public class FieldCutscenePause extends FieldCutscenePhase
    {
        private int msPauseDuration;
        private int msElapsedTime;

        public FieldCutscenePause(int msPauseDuration)
        {
            this.msPauseDuration = msPauseDuration;
        }

        @Override
        public void Begin(FieldCutscene parentCutscene)
        {
            super.Begin(parentCutscene);
            
            this.msElapsedTime = 0;
        }

        @Override
        public void Update(GameContainer gc, int delta)
        {
            this.msElapsedTime += delta;
            
            if (this.msElapsedTime >= this.msPauseDuration)
            {
                this.setIsFinished(true);
            }
        }

        @Override
        public void Draw(GameContainer gc, Graphics g)
        {
            // Nothing to draw - the scene itself is drawn by the cutscene.
        }
        
        @Override
        public void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("FieldCutscenePause");
            writer.WriteIntElement("MsPauseDuration", this.msPauseDuration);
            writer.EndElement();
        }

        private FieldCutscenePause(CaseXmlReader reader)
        {
            reader.StartElement("FieldCutscenePause");
            this.msPauseDuration = reader.ReadIntElement("MsPauseDuration");
            reader.EndElement();
        }
    }

    public class FieldCutscenePlayBgm extends FieldCutscenePhase
    {
        private String bgmId;

        public FieldCutscenePlayBgm(String bgmId)
        {
            this.bgmId = bgmId;
        }

        @Override
        public void Begin(FieldCutscene parentCutscene)
        {
            super.Begin(parentCutscene);
            Case.getInstance().getAudioManager().PlayBgmWithId(this.bgmId);
            this.setIsFinished(true);
        }

        @Override
        public void Update(GameContainer gc, int delta)
        {
        }

        @Override
        public void Draw(GameContainer gc, Graphics g)
        {
            // Nothing to draw - the scene itself is drawn by the cutscene.
        }
        
        @Override
        public void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("FieldCutscenePlayBgm");
            writer.WriteTextElement("BgmId", this.bgmId);
            writer.EndElement();
        }

        private FieldCutscenePlayBgm(CaseXmlReader reader)
        {
            reader.StartElement("FieldCutscenePlayBgm");
            this.bgmId = reader.ReadTextElement("BgmId");
            reader.EndElement();
        }
    }

    public class FieldCutsceneStopBgm extends FieldCutscenePhase
    {
        private boolean isInstant;
        
        public FieldCutsceneStopBgm(boolean isInstant)
        {
            this.isInstant = isInstant;
        }

        @Override
        public void Begin(FieldCutscene parentCutscene)
        {
            super.Begin(parentCutscene);
            Case.getInstance().getAudioManager().StopCurrentBgm();
            this.setIsFinished(true);
        }

        @Override
        public void Update(GameContainer gc, int delta)
        {
        }

        @Override
        public void Draw(GameContainer gc, Graphics g)
        {
            // Nothing to draw - the scene itself is drawn by the cutscene.
        }
        
        @Override
        public void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("FieldCutsceneStopBgm");
            writer.WriteBooleanElement("IsInstant", this.isInstant);
            writer.EndElement();
        }

        private FieldCutsceneStopBgm(CaseXmlReader reader)
        {
            reader.StartElement("FieldCutsceneStopBgm");
            this.isInstant = reader.ReadBooleanElement("IsInstant");
            reader.EndElement();
        }
    }

    public class FieldCutscenePlayAmbiance extends FieldCutscenePhase
    {
        private String ambianceSfxId;

        public FieldCutscenePlayAmbiance(String ambianceSfxId)
        {
            this.ambianceSfxId = ambianceSfxId;
        }

        @Override
        public void Begin(FieldCutscene parentCutscene)
        {
            super.Begin(parentCutscene);
            //Case.getInstance().getAudioManager().PlayAmbianceWithId(this.ambianceSfxId);
            this.setIsFinished(true);
        }

        @Override
        public void Update(GameContainer gc, int delta)
        {
        }

        @Override
        public void Draw(GameContainer gc, Graphics g)
        {
            // Nothing to draw - the scene itself is drawn by the cutscene.
        }
        
        @Override
        public void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("FieldCutscenePlayAmbiance");
            writer.WriteTextElement("AmbianceSfxId", this.ambianceSfxId);
            writer.EndElement();
        }

        private FieldCutscenePlayAmbiance(CaseXmlReader reader)
        {
            reader.StartElement("FieldCutscenePlayAmbiance");
            this.ambianceSfxId = reader.ReadTextElement("AmbianceSfxId");
            reader.EndElement();
        }
    }

    public class FieldCutsceneStopAmbiance extends FieldCutscenePhase
    {
        private boolean isInstant;
        
        public FieldCutsceneStopAmbiance(boolean isInstant)
        {
            this.isInstant = isInstant;
        }

        @Override
        public void Begin(FieldCutscene parentCutscene)
        {
            super.Begin(parentCutscene);
            //Case.getInstance().getAudioManager().StopCurrentAmbiance();
            this.setIsFinished(true);
        }

        @Override
        public void Update(GameContainer gc, int delta)
        {
        }

        @Override
        public void Draw(GameContainer gc, Graphics g)
        {
            // Nothing to draw - the scene itself is drawn by the cutscene.
        }
        
        @Override
        public void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("FieldCutsceneStopAmbiance");
            writer.WriteBooleanElement("IsInstant", this.isInstant);
            writer.EndElement();
        }

        private FieldCutsceneStopAmbiance(CaseXmlReader reader)
        {
            reader.StartElement("FieldCutsceneStopAmbiance");
            this.isInstant = reader.ReadBooleanElement("IsInstant");
            reader.EndElement();
        }
    }

    public class FieldCutsceneEndCase extends FieldCutscenePhase
    {
        private boolean completesCase;
        
        public FieldCutsceneEndCase(boolean completesCase)
        {
            this.completesCase = completesCase;
        }

        @Override
        public void Begin(FieldCutscene parentCutscene)
        {
            super.Begin(parentCutscene);
            
            Case.getInstance().setIsFinished(true);
        }

        @Override
        public void Update(GameContainer gc, int delta)
        {
            this.setIsFinished(true);
        }

        @Override
        public void Draw(GameContainer gc, Graphics g)
        {
            // Nothing to draw - the scene itself is drawn by the cutscene.
        }
        
        @Override
        public void SaveToXml(CaseXmlWriter writer) throws IOException
        {
            writer.StartElement("FieldCutsceneEndCase");
            writer.WriteBooleanElement("CompletesCase", this.completesCase);
            writer.EndElement();
        }

        private FieldCutsceneEndCase(CaseXmlReader reader)
        {
            reader.StartElement("FieldCutsceneEndCase");
            this.completesCase = reader.ReadBooleanElement("CompletesCase");
            reader.EndElement();
        }
    }
}